Skip to content

Validate CIMD scope, grant_types and response_types against AS policy#5385

Merged
amirejaz merged 5 commits into
mainfrom
cimd-phase2-pr6-cimd-validation
Jun 2, 2026
Merged

Validate CIMD scope, grant_types and response_types against AS policy#5385
amirejaz merged 5 commits into
mainfrom
cimd-phase2-pr6-cimd-validation

Conversation

@amirejaz
Copy link
Copy Markdown
Contributor

Summary

C3 — CIMD scope handling now consistent with DCR:
ScopesSupported is threaded into NewCIMDStorageDecorator. Scope validation in fetch() now uses registration.ValidateScopes — the same function as the DCR handler — so both paths enforce the same scope policy. A CIMD document declaring scopes outside ScopesSupported is rejected at GetClient time. The fallback when scope is omitted also respects ScopesSupported.

C4 — grant_types / response_types validated at fetch time:
fetch() now rejects documents declaring grant_types outside [authorization_code, refresh_token] or response_types outside [code]. Consistent with DCR which returns invalid_client_metadata for the same cases, and matches the existing token_endpoint_auth_method validation pattern already in the decorator.

Type of change

  • Bug fix / security improvement

Test plan

  • go test ./pkg/authserver/... passes
  • task lint-fix clean
  • New tests: scope validation with/without ScopesSupported, grant_type rejection for unsupported values, response_type rejection for token

Generated with Claude Code

@amirejaz amirejaz force-pushed the cimd-phase2-pr6-cimd-validation branch from 1e060ed to 90db2a6 Compare May 26, 2026 21:08
@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label May 26, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.77%. Comparing base (16ce897) to head (969544a).

Files with missing lines Patch % Lines
pkg/authserver/server_impl.go 58.33% 4 Missing and 1 partial ⚠️
pkg/authserver/storage/cimd_decorator.go 95.34% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5385      +/-   ##
==========================================
+ Coverage   68.76%   68.77%   +0.01%     
==========================================
  Files         634      633       -1     
  Lines       64183    64229      +46     
==========================================
+ Hits        44136    44175      +39     
- Misses      16778    16784       +6     
- Partials     3269     3270       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@amirejaz amirejaz force-pushed the cimd-phase2-pr6-cimd-validation branch from 90db2a6 to 070de2d Compare May 26, 2026 21:23
@amirejaz amirejaz marked this pull request as ready for review May 26, 2026 21:23
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/S Small PR: 100-299 lines changed labels May 26, 2026
@amirejaz amirejaz force-pushed the cimd-phase2-pr6-cimd-validation branch from 070de2d to 8e68d45 Compare May 26, 2026 21:38
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels May 26, 2026
C3 - Thread ScopesSupported into NewCIMDStorageDecorator so CIMD scope
     handling is consistent with DCR. Uses registration.ValidateScopes
     (same function as the DCR handler) to validate declared scopes
     against the AS allowlist and compute the effective scope list.
     When ScopesSupported is unset, the document's declared scopes are
     used directly; omitted scopes default to DefaultScopes.

C4 - Reject CIMD documents that declare grant_types or response_types
     the embedded AS does not support for public clients
     (authorization_code + refresh_token; code). Consistent with DCR
     which returns invalid_client_metadata for the same cases.

buildFositeClient now receives pre-computed scopes from fetch() rather
than re-parsing doc.Scope, matching the DCR handler pattern where scope
computation and validation happen before client construction.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@amirejaz amirejaz force-pushed the cimd-phase2-pr6-cimd-validation branch from 8e68d45 to 36b5e97 Compare May 26, 2026 21:40
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels May 26, 2026
Copy link
Copy Markdown
Contributor

@tgrunnagle tgrunnagle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review

Four specialist agents (oauth/security, Go correctness/style, test quality, general/API) reviewed this PR. Codex cross-review was skipped (codex CLI not installed locally). 0 HIGH, 5 MEDIUM, 7 LOW findings survived consensus scoring. None are blocking; this is posted as COMMENT.

Executive summary

A focused follow-up to stacked PR #5348 closing deferred review comments C3/C4. It threads ScopesSupported and BaselineClientScopes into the CIMD storage decorator so CIMD documents are validated against the same policy as DCR (via registration.ValidateScopes), and rejects unsupported grant_types / response_types at fetch time. As supporting refactor, unionScopes is promoted to registration.UnionScopes. Approach is correct in shape: reuse a single validator, reject earlier in the pipeline, surface a startup warning for the CIMD+baseline interaction. Most surviving findings are correctness-leaning details and test-quality nits.

Pros

  • Reuses registration.ValidateScopes instead of duplicating policy.
  • Rejects invalid CIMD documents at fetch time (not at /authorize), giving operators a clear, early error.
  • Error class change ErrNotFoundErrInvalidClient for "fetched but policy-violating" is semantically correct per RFC 6749 §5.2.
  • Public/PKCE invariant preserved (Public: true unconditional; allowlists still exclude client_credentials).
  • slog.Warn at startup surfaces the non-obvious CIMD+baseline interaction.
  • Doc comments explain the design intent and the test-only nil escape hatch.
  • PR title, body, and size comply with .claude/rules/pr-creation.md.

Cons

  • The "same scope policy as DCR" claim has one silent divergence: when doc.Scope == "" and ScopesSupportedDefaultScopes, the CIMD client receives the full ScopesSupported while a DCR client in the same position receives only DefaultScopes. See F5.
  • NewCIMDStorageDecorator now takes six positional args with two adjacent same-typed []string slices — easy to swap silently.
  • TestUnionScopes was not relocated alongside the moved function and still lives in pkg/authserver/server/handlers/, where scopes.go is now empty.
  • New rejection tests assert require.Error(...) only — they would still pass if the code regressed to fosite.ErrNotFound, defeating the PR's main intent.

Consensus findings table

# Severity File Title
F1 MEDIUM handlers/scopes_test.go TestUnionScopes lives in wrong package after move; pair with deleting now-empty scopes.go
F2 MEDIUM storage/cimd_decorator_test.go Rejection tests don't assert errors.Is(err, fosite.ErrInvalidClient)
F3 MEDIUM storage/cimd_decorator_test.go Consolidate 11 new top-level tests into ~3 table-driven tests
F4 MEDIUM storage/cimd_decorator.go NewCIMDStorageDecorator signature: 6 positional args, two adjacent same-typed slices
F5 MEDIUM storage/cimd_decorator.go Omitted-scope branch on CIMD diverges from DCR when ScopesSupportedDefaultScopes
F6 LOW storage/cimd_decorator.go WithHint(dcrErr.Error) passes OAuth error code where descriptive hint belongs
F7 LOW storage/cimd_decorator.go Constructor stores caller-supplied scope slices without defensive slices.Clone
F8 LOW storage/cimd_decorator.go Doc comments document cross-file invariants not enforced in code
F9 LOW storage/cimd_decorator.go Missing DCR-parity defense-in-depth: response_types must include 'code'; grant-type validation order reversed vs DCR
F10 LOW storage/cimd_decorator_test.go Test names don't match assertions
F11 LOW storage/cimd_decorator_test.go Redundant unit-level test; missing baseline-exceeds-supported coverage
F12 LOW authserver/server_impl.go slog.Warn level/clarity for CIMD+baseline interaction

Detailed comments are inline on the relevant changed lines.

How to test

git fetch origin pull/5385/head:pr-5385 && git checkout pr-5385
task lint-fix
task test

Plus targeted checks against an embedded AS:

  1. C3 — declared scope outside ScopesSupported: serve a CIMD doc declaring scope: "openid profile" with scopes_supported: ["openid"]. Expect fosite.ErrInvalidClient.
  2. C3 — omitted scope: same setup but doc omits scope. Confirm the client carries exactly ["openid"], not DefaultScopes (and consider what should happen when ScopesSupportedDefaultScopes — see F5).
  3. C4 — unsupported grant_type: serve a doc with grant_types: ["client_credentials"]. Expect rejection.
  4. C4 — missing authorization_code: serve a doc with grant_types: ["refresh_token"] only. Expect rejection.
  5. C4 — unsupported response_type: serve a doc with response_types: ["token"]. Expect rejection.
  6. slog.Warn: start the AS with cimd.enabled: true and baseline_client_scopes: ["offline_access"]. Expect one WARN line at startup.
  7. Load-bearing-test check: temporarily revert if !allowedCIMDGrantTypes[gt] to always-false, re-run go test ./pkg/authserver/storage/.... TestFetch_RejectsUnsupportedGrantType should fail; revert when done.

Generated with Claude Code (multi-agent /dev:pr-review).

Comment thread pkg/authserver/server/handlers/scopes_test.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator_test.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator_test.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator_test.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator_test.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator.go
Comment thread pkg/authserver/storage/cimd_decorator.go Outdated
Comment thread pkg/authserver/storage/cimd_decorator.go Outdated
Comment thread pkg/authserver/server_impl.go
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 1, 2026
F1  Move TestUnionScopes to registration package where UnionScopes lives;
    delete now-empty handlers/scopes.go and handlers/scopes_test.go
F2  Add assert.ErrorIs(ErrInvalidClient)/NotErrorIs(ErrNotFound) to
    all CIMD policy rejection tests to pin the error type change
F4  Replace 6 positional NewCIMDStorageDecorator args with
    CIMDDecoratorConfig struct — prevents silent swap of adjacent []string
F5  Omitted-scope now calls ValidateScopes(nil, scopesSupported) matching
    DCR: returns DefaultScopes when DefaultScopes ⊆ ScopesSupported,
    error otherwise (document must declare scope explicitly)
F6  Fix dcrErr.Error → dcrErr.ErrorDescription in scope validation hint
    so the human-readable description reaches the fosite hint field
F7  slices.Clone scope slices in CIMDDecoratorConfig constructor
F8  Fix buildFositeClient doc: "when empty" not "when nil"
F9  Export ValidatePublicGrantTypes/ValidatePublicResponseTypes from
    registration package; replace hand-rolled loops in cimd_decorator.go
    with calls to these shared validators — grant_type/response_type
    validation is now identical on both DCR and CIMD paths
F10 Rename AcceptsSupportedGrantTypes→AcceptsOmittedGrantTypes and
    RejectsRefreshTokenOnly→RejectsGrantTypesMissingAuthorizationCode
F11 Remove redundant TestBuildFositeClient_ScopeDefaultsToScopesSupported
    (real decision lives in fetch(), not buildFositeClient)
F12 Update slog.Warn message to name the security consequence when
    CIMD+BaselineClientScopes are both configured

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@amirejaz amirejaz force-pushed the cimd-phase2-pr6-cimd-validation branch from 1dd4677 to 7c14ee2 Compare June 2, 2026 11:20
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 2, 2026
@github-actions github-actions Bot removed the size/L Large PR: 600-999 lines changed label Jun 2, 2026
@github-actions github-actions Bot added the size/L Large PR: 600-999 lines changed label Jun 2, 2026
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 2, 2026
Copy link
Copy Markdown
Contributor

@tgrunnagle tgrunnagle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review LGTM — all 12 findings from the prior multi-agent review are resolved. F3 went from 11 separate tests to 3 table-driven tests (plus a serveCIMDDocWithFields helper), F5 chose the fail-closed option (require explicit scope when DefaultScopes ⊄ ScopesSupported), and F9 extracted ValidatePublicGrantTypes / ValidatePublicResponseTypes so DCR and CIMD now share a single validation source. The errors.Is(err, fosite.ErrInvalidClient) assertions pin the error-type change so a regression to ErrNotFound would fail tests.

One nice-to-have follow-up (not blocking)

defaultCIMDGrantTypes and defaultCIMDResponseTypes in cimd_decorator.go are still parallel to defaultGrantTypes / defaultResponseTypes in the registration package. After F9 the validation is shared, but the default application is still duplicated — if someone later widens registration.defaultGrantTypes for DCR (e.g., adds a new public-client grant), they'd need to remember to update the CIMD copy too, and task lint won't catch the mismatch.

Since ValidatePublicGrantTypes / ValidatePublicResponseTypes already return the defaulted slice, buildFositeClient could use those return values and the CIMD-side constants could be deleted. Closes the last DCR/CIMD parity gap. Not blocking for this PR — could be a small follow-up.

Approving.

@amirejaz amirejaz merged commit 521630b into main Jun 2, 2026
45 checks passed
@amirejaz amirejaz deleted the cimd-phase2-pr6-cimd-validation branch June 2, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants